feat(auth)!: adopt better-auth's account-issuer rollback — drop sys_account.issuer, retire the backfill, lift the family to 1.7.3 - #17454
Conversation
better-auth 1.7.3 removed the issuer-scoped account identity outright (better-auth/better-auth#10909). #16186 held the family at an exact 1.7.2 as a stopgap; this is the durable half — the family moves as ONE line, since @better-auth/core@1.7.2 and @better-auth/kysely-adapter@1.7.3 are mutually incompatible in both directions. The exact-target rule is unchanged and stays exact for the reason 1.7.3 itself demonstrated: this vendor deletes public exports in patch releases. Refs #17440. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e-point guard The preflight reads sys_account ROWS (never the index declaration) and refuses when a (provider_id, account_id) key is held by more than one row -- the class that is legal under the retired (issuer, account_id) key and is ONE account under the key better-auth 1.7.3 restored. Two reads that are NOT reported as clean: one that throws, and a walk that truncates. The retired backfill swallowed both; correct there, wrong for an answer that authorises an irreversible drop. The re-point guard answers the one case issuer still discriminated: a provider_id re-pointed at another IdP must have its account bindings rebuilt, because no column records which IdP vouched for a row. Refs #17440. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…point answer 15 cases. Every refusal asserts the ADR-0112 envelope (code + status) and the substance of the message -- a bare toThrow() would pass on a fixture that never reached the probe. The collision fixture registers an INDEX-LESS sys_account on purpose: the PREMISE case proves the class cannot be inserted where the long-declared (provider_id, account_id) UNIQUE is physically present, so the only population that can hold it is a deployment carrying the declaration without the constraint (#14902 / #15479). Refs #17440. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…table The read-only pre-flight leg ADR-0120 D4 requires on a NARROWING index change, plus a refusal in os migrate apply that sits BELOW the report and ABOVE both writes -- so the column drop cannot proceed on a database holding the collision class. No second ceremony: plan/backup/apply/post-check and the boot refusal all already exist for a column drop (os migrate plan, the operator's backup, os migrate apply --allow-destructive, runArtifactBootMigrationGate). Only the row-level pre-flight was missing. Refs #17440. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… gate 1.7.3 added the #10700 gate one layer above ours: /two-factor/enable now throws TOTP_ALREADY_ENABLED when a two-factor row exists with verified !== false. Measured: 0 files carry that code in 1.7.2, 5 in 1.7.3, against a control code present in both. The re-enrollment legs now assert the upstream refusal envelope plus the property behind it (nothing rotated behind the refusal); the #10700 inertness assertion moves to the unconfirmed path, which is the one upstream's gate still admits; rotation moves to disable -> enable -> confirm, carrying the cookie disable installs. Upstream's gate READS verified -- the field #10700 was about -- so two-factor-reenrollment-verified-reset.ts keeps that gate's input truthful and is NOT dead code superseded by the vendor. Also drops the retired issuer mapping from the two account-config pins. Refs #17440. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four generated translation bundles lose the sys_account.issuer label (regenerated via pnpm i18n:extract, 4 files x 4 lines). The client's accounts.list type drops the field the route no longer returns. The showcase seed drops the issuer derivation, its failure branch and the whole silent-lockout class behind it. The dogfood personas test keeps its file and its real-HTTP-sign-in half; the issuer assertion is REPLACED by the key sign-in now resolves on, with the header recording verbatim what the old assertion said and why it went away. It additionally asserts the retired column is ABSENT. platform-keyed-text-bounds's live illustration moves to a MEASURED surviving member of the >768 unique class. Refs #17440. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…LIVE edge The retired specimen was @better-auth/core/db naming createLocalAccountIssuer -- the #16186 defect -- and the case carried the instruction 'if the durable fix landed, retire this case with it'. It has landed. Retiring the SPECIMEN is not retiring the case: what it catches is a collector that has silently stopped reaching publishable source, which is how #16186 passed over nothing for three releases. So it re-anchors on better-auth/adapters -> createAdapterFactory and still asserts a NAMED symbol, and a new case asserts the two deleted names are imported nowhere. ⛔ The gate is not loosened: it still requires an exact declared range, agreement with the installed version, and real resolution of every named symbol. Refs #17440. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…etirement Registers sys-account-issuer-retired under protocol major 18 and regenerates registry.ts and both projections. The changeset carries the BREAKING banner, the adr-0087 disposition marker and the FROM -> TO table, and is graded minor under the launch-window convention (check-changeset-no-major refuses major). Refs #17440. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ator check:nul-bytes caught two raw NUL bytes in the preflight's composite map key -- an escape materialised into the real byte while the file was being written, which is the exact slip that gate's header documents. A raw NUL renders as NOTHING, so a load-bearing separator reads in grep and in review as an empty string. JSON.stringify([providerId, accountId]) has no delimiter ambiguity and no control byte at all. Refs #17440. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l database Two engines over one SQLite file: engine A declares issuer (the pre-upgrade shape) and a real AuthManager signs a user up through the real HTTP route so the hash is better-auth's own, then the row is stamped the way a 1.7.2 runtime stamped it; engine B on the SAME file registers today's objects -- new code, old table. Four cases: the legacy account still signs in over the real auth route (judged by the principal the session resolves to, never by a status); schema sync does NOT silently drop the undeclared column, so the drop stays the operator's deliberate act; the pre-flight reads clean, which is what authorises it; and sign-in still works once the column is gone. Both PRAGMA reads carry a control -- not.toContain passes vacuously on an empty array, which is the one reading this must never produce by accident. Refs #17440. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ription check:doc-authoring: a command description reaches operators, who have no tracker to resolve #NNNN against. The id moves to an adjacent comment, where the reader who can resolve it already is. Refs #17440. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 45 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 145 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 386e5676b1d09322630ecf66758d2c21d14f35a4 && git checkout 386e5676b1d09322630ecf66758d2c21d14f35a4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 431c757120907a7b166e5257df7447ad80261eef 5f16ab6a4317417d1c077c297ef37ba09900af41 && git checkout -B drift-repro 431c757120907a7b166e5257df7447ad80261eef && git merge --no-ff 5f16ab6a4317417d1c077c297ef37ba09900af41
node scripts/docs-audit/affected-docs.mjs --json 431c757120907a7b166e5257df7447ad80261eef
|
…lder Both conflicts are generated tenant-audit census artefacts: content/docs/permissions/tenant-audit-census.mdx and docs/audits/2026-08-tenant-audit-write-call-sites.counts.md. main re-ran the census while this branch must regenerate it too (it deletes a write call site), so both sides rewrote the same rows. Resolved to main's side as a PLACEHOLDER only, so that the merge is committed BEFORE anything is regenerated - regenerating while the tree is still in MERGE state is the ordering trap scripts/pm/os-regen-merge.sh exists to prevent. The regeneration and the re-derived prose follow in the next commit; the numbers in this commit are not the measured ones. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main re-ran the census while this branch also has to regenerate it (it deletes
a write call site), so both sides rewrote the same rows. The merge commit took
main's side as a placeholder; this commit carries the real measurement.
Regenerated with `node scripts/tenant-audit-census.mjs --write`, then every
prose figure re-derived FROM THE REGENERATED CENSUS rather than carried
forward from an earlier round: the population moved 223 -> 222, statically
decidable 149 -> 148, decidably elevated 105 -> 104, and the erased-receiver
count 45 -> 44 (18 + 15 + 11).
Re-checked the class no gate sees - sentences that ride on a figure without
quoting it, so no gate reads them:
- "better than a fifth" went FALSE and is corrected to "just under a fifth":
44 of 222 is 19.8%, and a fifth of 222 is 44.4.
- "a third of this population is undecidable in one dimension or another"
still holds: 74 of 222 is 33.3%.
- the unread "(47%)" beside "104 of 222" still rounds true: 46.85%.
node scripts/check-tenant-audit-census.mjs exits 0 - "222 write call sites
certified (148 decidable; 9 tenancy-enabled sites PROVABLY carry no tenant
context, 32 more unreadable), 23 prose figures held to the census" - and
--self-test exits 0 with 18 + 19 cases.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes #17440
Maintainer ruling 2026-09-10 on #16629, option 1: adopt better-auth's account-issuer
rollback.
sys_account.issuerretires with the backfill that served it, and the@better-auth/*family lifts to an exact1.7.3in one line.Verified at
e577e0eb4.⭐ The finding that shaped the migration
The card asks for a pre-flight that detects rows sharing
provider_id+account_idand differing only in
issuer. Measuring the premise first changed how that pre-flighthad to be built:
sys_accounthas declared{ fields: ['provider_id', 'account_id'], unique: true }since the object was created.
git log -Sputs it in the commit that created theobject; the
(issuer, account_id)pair arrived much later, with the 1.7.0-rc.2 bump(#3632). So the "new" key is not new — it long predates the column being dropped, and
wherever that index is physically present the collision class is refused at write time.
That does not make the pre-flight unnecessary. It makes one thing about it load-bearing:
syncDeclaredIndexeslogs a plain UNIQUE whoseCREATE fails on existing duplicates onto the durability channel and lets the boot
continue (#14902 / #15479) — deliberately, so one dirty table cannot take a deployment
down. A database that ever held duplicates therefore carries the declaration and not the
constraint, and can still hold the class today.
⇒ On such a database the drop does not blow up. It degrades silently: the rows become
indistinguishable,
findAccountByKeyresolves whichever the driver hands back first, anda sign-in can land on the wrong user's account. That is strictly worse than a failed
apply, and it is why the pre-flight reads rows, never the index declaration.
The ceremony — reused, not invented
ADR-0131 D10 fixes the shape and says in the same breath that it "reuses the ADR-0120 D4
migration ceremony where it exists (index and column changes) rather than inventing a
second one." A column drop plus an index re-key is exactly ADR-0120 D4's class, and this
repository already ships every leg of it:
os migrate planreports the drop as destructive driftos migrate account-issuer— read-only, exits non-zeroos migrate apply --allow-destructive, which now refuses this drop while the pre-flight is dirtyos migrate account-issuer; it reads zerorunArtifactBootMigrationGatealready fails the boot on unapplied destructive drift, naming the command;os servenever auto-migratesSo the smallest honest addition was the read-only pre-flight D4 asks for on a narrowing
index change, plus a refusal in front of the drop. An
os migrate account-issuer --applythat dropped the column itself would be the second ceremony D10 forbids, and it would drop
a column outside the drift reconciler that owns every other column drop.
⛔ No
sys_migrationflag, deliberately —os migrate summary-nullsdocuments the rulethat a deployment flag nothing reads is a fact nothing reads. The consumer of this verdict
is the gate in
os migrate apply, which re-runs the probe against the live database at themoment it matters; a row saying "clean on Tuesday" authorises nothing on Thursday.
Refusal discipline
Two readings are deliberately not reported as clean, because a pre-flight that cannot
see is not a pre-flight that found nothing:
backfill-account-issuer.tswrapped itsreads in
try { … } catch { return [] }— correct for an idempotent best-effort passthat runs again next boot, and exactly wrong for an answer that authorises an
irreversible drop.
⛔ Nothing is merged or deleted for the operator: which row survives is application
knowledge, and two different people can be behind one colliding key.
A
provider_idre-pointed at a different IdP must have its account bindings REBUILT. Nokey separates them, and after the column drop nothing can.
sys_sso_providerdeclares{ fields: ['provider_id'], unique: true }, so within anenvironment
provider_id → issueris a function and(provider_id, account_id)determines what
(issuer, account_id)determined — for as long as that function holds.Re-pointing breaks it. If the new IdP mints a
subthe old one had already issued tosomebody else, the new key resolves that sign-in onto the other person's account row.
findAccountByKeymissed the old row,better-auth tried to insert, and the long-standing
(provider_id, account_id)uniquerefused it — the user saw
unable_to_link_account. Under the new key it resolvessilently. The narrowing turns a loud refusal into a quiet cross-user sign-in, which is why
this is answered rather than left to a constraint.
Enforced at the re-point, because that is the last moment the distinction exists. After
the drop no column records which IdP vouched for a row, so no runtime check can tell an old
binding from a new one.
refuseIssuerRepointWithLiveBindingssits on thesys_sso_providerupdate doors and declines an issuer change while accounts are still bound to that
provider_id(RESOURCE_CONFLICT/ 409). The operator deletes the stale bindings; eachuser re-links on their next sign-in.
Pinned by five cases, including the one that states the answer directly — two rows under
one
provider_iddiffering only in issuer are one key, two issuers, two people.The two flagged items
showcase-demo-personas-loginable.dogfood.test.tskeeps its file and its real half.The issuer assertion is replaced, not dropped: its job was "the account is resolvable
under the key sign-in uses", and the key is now
(provider_id, account_id)— so that iswhat it asserts, with the admin's own better-auth-minted account as the same positive
control the issuer case carried, plus a new assertion that the retired column is absent.
The header quotes the old assertion verbatim and records why it went away, so the trap that
bit four checklist items is not lost with the field that caused it.
check:vendor-export-contractis not loosened. It still requires an exact declaredrange, agreement with the installed version, and real resolution of every named symbol.
Its self-test carried the instruction "if the durable fix landed, retire this case with
it" — this is that fix. ⛔ Retiring the specimen is not retiring the case: what it
catches is a collector that has silently stopped reaching publishable source, which is how
#16186 passed over nothing for three releases. So it re-anchors on a live edge
(
better-auth/adapters→createAdapterFactory) and still asserts a named symbol, anda new case asserts the two deleted names are imported nowhere.
Out of scope, untouched
⛔ #11627's hash-shadow-key machinery stays — a generic driver capability serving five
UNIQUE members of the >768-char class. The one place it was cited as an illustration
(
platform-keyed-text-bounds.test.ts) moves to a measured surviving member of thatclass,
sys_oauth_access_token.token(1024), rather than a plausible-looking name.Verification
scripts/pm/os-verify-lock.shcouldnot take the shared verify lock on this host: no usable
flock. The shared verify lock isdeclared Linux-only (
flockis util-linux, and a stock macOS does not ship it), so thecommands below were run directly, without the lock — a declared narrowing, not a silent
one. No serialization guarantee held for these runs.
TMPDIRwas pointed at a non-symlinked path for the CLI and dogfoodsuites. On macOS
/varis a symlink to/private/var, and ten CLI cases compare a paththe test itself built from
tmpdir()against the realpath Node returns. Proven to be thehost and not this diff: the same three files, unchanged on this branch, pass 41/41 under
TMPDIR=/private/tmp/…. CI runs on Linux, where/tmpis not symlinked.Acceptance
① The pre-flight refuses on a fixture containing the collision class — watched refusing.
Every refusal asserts the ADR-0112 envelope (
codeandstatus) and the substance ofthe message — never a bare
toThrow(), which would pass on a fixture that never reachedthe probe.
⭐ The fixture registers an index-less
sys_accounton purpose, and the file says why.The
PREMISEcase proves the class cannot be inserted where the declared unique isphysically present, by trying against the real object and watching the driver refuse
(with a control: a different
account_idinserts fine). So the only population that canhold the class is a deployment carrying the declaration without the constraint — which is
exactly what the fixture models.
② Fresh install and existing-data upgrade both end with working sign-in over a real auth
route.
Fresh — the real showcase boot:
Existing data — two engines over one SQLite file (engine A declares
issuerand signs auser up through the real HTTP route so the hash is better-auth's own; engine B on the same
file registers today's objects: new code, old table):
Both sign-ins are judged by the principal the session resolves to, never by a status. Both
PRAGMAreads carry a control —not.toContainpasses vacuously on an empty array, whichis the one reading this must never produce by accident.
③ The re-pointed-provider answer is stated and pinned — stated above, pinned by the five
cases in
account-identity-preflight.test.ts.④
check:vendor-export-contract— both directions proven.Passing at
1.7.3:Still failing when pointed at a symbol the pinned version does not export — an ablation on
the committed tree, mutation confirmed on disk before the measurement and the restore proven
by hash:
No rebuild leg is owed: this gate parses publishable source and resolves against
node_modules, so nodist/sits between the mutation and the verdict.⑤ The changeset carries its ADR-0087 disposition and the FROM → TO mapping.
The changeset carries the disposition marker naming
sys-account-issuer-retired(spelled as theHTML comment the gate reads; not reproduced here, because this body's sanitizer eats
angle-bracket fragments). The entry is added under
protocol major 18 and
registry.tsplus both projections regenerated.minor, notmajor. The dispatch card asked for a major arm;check-changeset-no-majorrefuses a major in this launch window, and the live conventioncarries breaking-ness with a BREAKING banner plus the ADR-0087 disposition. Flagged
rather than silently chosen.
Suites and gates
Gates run locally, all exit 0:
check:nul-bytes·check:vendor-export-contract·check:adr-0087-registration·check:override-consistency·check:changeset-gate-self-tests·check:error-code-casing·check:doc-authoring·check:i18n·check:i18n-coverage·check:i18n-stale-fill·check:i18n-walk-parity·check:cli-command-ids·check:cli-examples-parity·check:test-source-alias·check:cross-package-test-inputs·check:engine-double-contract·check:dts-closure·check:published-readme-exports·check:pm-widening-tells·check:single-claim-paths·check:route-envelope·check:error-status-conformance·check:agent-test-spelling·check:pm-governed-prose·check:partof-closing-keyword, and in@objectstack/spec:check:migration-registry·check:spec-changes·check:upgrade-guide·check:api-surface·check:export-origins·check:exported-any·check:liveness·check:authorable-surface.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackderives 110families for this change set and was re-derived after the diff grew (no new families). The
remainder is the repo-wide farm, which CI runs exactly once — a declared narrowing, not an
omission.
check:nul-bytescaught a real defect of mine mid-run: two raw NUL bytes in thepre-flight's composite map key, an escape materialised into the byte while the file was
being written. The key is now
JSON.stringify([providerId, accountId])— no delimiterambiguity and no control byte at all.
A neighbouring behaviour change the family lift brought with it
better-auth
1.7.3added the #10700 gate one layer above ours:/two-factor/enablethrowsTOTP_ALREADY_ENABLEDwhen a two-factor row exists withverified !== false. Measured:that code appears in 0 files in 1.7.2 and 5 in 1.7.3, against a control code present
in both (5 / 5).
⭐ Upstream's gate READS
verified— the exact field #10700 was about — sotwo-factor-reenrollment-verified-reset.tsis what keeps that gate's input truthful. ⛔ Itis not dead code superseded by the vendor. The re-enrollment legs now assert the upstream
refusal envelope plus the property behind it (nothing rotated behind the refusal); the
inertness assertion moves to the unconfirmed path, which is the one upstream's gate still
admits; and rotation moves to
disable → enable → confirm.Merged with
main— the conflict that was running zero CIThis PR sat conflicting, and a conflicting PR runs nothing:
mergeable=false,mergeable_state=dirty, 0 workflow runs on its head. Its check list was theprevious head's and said nothing about this one. Two census artefacts conflicted,
both generated —
content/docs/permissions/tenant-audit-census.mdxanddocs/audits/2026-08-tenant-audit-write-call-sites.counts.md. main's2a79726ac(#17436, verified here rather than taken on trust) had independently re-run the
same census, so both sides rewrote the same
Measured online and the samecorpus-scale table.
⭐ The order is fixed and it is not the obvious one — regenerating while the
tree is still in MERGE state rolls a generated anchor back to the branch's old
fork point, and a rolled-back artefact is still authentic, so every gate passes
while a landed advance is quietly undone.
scripts/pm/os-regen-merge.shmechanises the right order and was used:
bash scripts/pm/os-regen-merge.sh— fetched, merged, and stopped exactlywhere it should: neither conflicted path is routed to the
merge=os-regendriver, and the script refuses to resolve non-generated files on your behalf.
⭐ Measured before resolving anything: of the six paths changed on both
sides of this merge, zero are os-regen paths — so the driver's silent-drop
hazard did not apply here and the script's step 2 had no work to do. ⛔ It was
deliberately not re-run after the merge commit: its own header says the
base must be read BEFORE step 1, and afterwards
git merge-base HEAD origin/mainis main's own tip, which makes step 2 inert for the wrong reason.— a placeholder, and the commit message says so, because a census block is an
answer to a tree and the merged tree is neither side's.
node scripts/tenant-audit-census.mjs --write, on the committed merge.Re-derived, never carried forward
⛔ No figure below was copied from a CI log, from the pre-merge branch, or from
main. A script imported the gate's own
PROSE_COUNTS, applied the samesplitPageplus whitespace normalisation the gate applies — the page ishard-wrapped at 80 columns, so an un-normalised match is a false NO MATCH, which
is how six rows first read as missing — and evaluated
expected(census)againstthe page for all 23 enforced rows:
Only corpus scale moved:
engine-shaped types recognised58 to 59, plus thedated marker.
sources scanned562,declared objects300 andnon-engine calls137 arrived with main's re-run and the merged tree reproduces all three.The population held exactly still — 222 write call sites, 148 decidable, 9
provable-and-tenancy-enabled, 32 unreadable — which is why no enforced prose
figure needed an edit. That is a measurement, not an assumption.
see — were re-checked against the same census:
44 of 222reached through an erased receiver is 19.8%, and a fifth of 222is 44.4, so "just under a fifth" still holds. main's side of that sentence
reads "better than a fifth" at
45 of 222: true for main's tree, false forthe merged one. The auto-merge kept the branch's corrected wording, and this is
the reading that confirms it.
104 of 222decidably elevated is 46.9%, so the quoted(47%)still roundstrue. The gate captures the count and the population out of that sentence and
leaves the per cent unread.
Across 300 declared objectsis the one UNENFORCED prose figure — required tobe present, never compared. It came in from main's side and the regenerated
scale row agrees with it.
node scripts/check-tenant-audit-census.mjsand its--self-testboth exit 0 onthe merged tree.
The ten version stamps the 1.7.3 lift falsified
check:vendor-version-stampswas red, and it is this PR's own doing — the samegate exits 0 on an unmodified
maincheckout. CI had not reported it because thelint job fail-fasts on the census check, roughly 900 lines earlier.
⛔ Not a
1.7.2to1.7.3substitution. The gate's own reason: a stampattests that a behaviour was MEASURED against the version it names, so changing
the number without redoing the measurement manufactures a claim nobody made,
which is worse than a stale one. The ten sites were judged one at a time, and
they split 7 / 3.
Route (a) — re-measured against the installed 1.7.3, then restamped AND dated
Seven sites whose claim is a static reading of the vendor's published files.
Cheap to take again and worth taking, because a family lift is precisely the
event that could invalidate one. All seven came back unchanged:
packages/cli/src/commands/init.ts:178@better-auth/scim@1.7.3still peers@better-auth/utils@0.4.2exactly, off the installed manifestpackages/cli/src/commands/init.ts:509packages/plugins/plugin-auth/src/auth-schema-config.ts:954SCIMOptionsstill declares noschema/modelName/fields— the same six memberspackages/plugins/plugin-auth/src/list-user-invitations-verification.ts:11crud-invites.mjsstill asks the helper on the three id-addressed routes and still throws unconditionally inlistUserInvitations, so the defect this file repairs is still minted upstreampackages/plugins/plugin-auth/src/auth-email-locale.test.ts:869/sign-in/magic-linkstill sends with no user lookup;/magic-link/verifystill creates the user unlessdisableSignUppackages/plugins/plugin-auth/src/auth-email-locale.test.ts:1001signInMagicLinkBodySchemais stillz.email()with no case transform;findUserByEmailstill matches onemail.toLowerCase()packages/plugins/plugin-auth/src/auth-manager.ts:3888db/adapter-base.mjsstill buildsmemoryDBfromObject.keys(tables)— the schema KEY — while@better-auth/memory-adapterstill resolves by model name and throwsinit.ts:509said better-sqlite3 is referenced by "no file in the publishedpackage at all", and
package.jsonis a file that references it. It now readsexcept that peer declaration itself.
Route (b) — anchored, deliberately NOT restamped
Three sites whose reading came from a drive, not from a file. Restamping
these would assert a drive nobody re-ran.
packages/client/src/index.ts:3656AuthManagerplusSqlDriver. Anchored to the date and card that took it (2026-09-09, #16761), scoped to "the then-installed 1.7.2", and the sentence now says out loud that the drive has not been re-run against the lifted familypackages/plugins/plugin-auth/src/scim-connection-service.ts:55enterWithlosing the store. Re-measuring would mean re-breaking the scope to watch it fail again. Anchored to 2026-09-02 / #14624, and the sentence now points atscim-transaction-scope.test.ts, which pins the SHIPPED behaviour at run time against whatever version is installedpackages/plugins/plugin-auth/src/account-issuer-upgrade-path.test.ts:27Proof that the repair changed prose and not behaviour
Seven of the eight touched files are provably comment-only: each was
transpiled with
removeCommentsat HEAD and at the working copy, and the emittedJS hashes are equal — with a
consttoletcontrol on every file proving theinstrument can say no, so "identical" is not a vacuous verdict. ⛔ A raw scanner
is not sound for this question (template literals and regex-versus-division
need parser context); the first attempt using one reported three false
differences before it was replaced with a real parse and emit.
packages/cli/src/commands/init.tsis the exception by design — its stamplives in string literals the scaffold writes into a user's project, so it is a
real change to emitted content. Its scaffold tests were therefore run:
Gates, at the commit that carries them
Union re-run after the final commit,
a760606a6, all exiting 0:check-tenant-audit-censusand its--self-test·check:vendor-version-stamps(self-test 64 checks, then 6980 files scanned) ·
check:nul-bytes·check:doc-authoring·check:corpus-claim-drift·check:pm-governed-prose·check:scaffold-emission-policy·check:cli-examples-parity·check:type-check-coverage·check:type-check-debt.Repo-wide
pnpm lintexits 0 (24s — not narrowed, so no narrowing needsdeclaring).
typecheckgreen on@objectstack/cli,@objectstack/clientand@objectstack/plugin-auth, over freshly built dependency closures.scripts/pm/os-verify-lock.shreportsNO USABLE flockon this host — theshared verify lock is Linux-only — so every heavy command was routed through the
entry point and ran in its declared unlocked mode, each printing
VERDICT command-exit 0 · UNLOCKED (declared). No serialization guarantee held.stamp repairs heads route (a) with "six sites" and then lists seven. The split is
7 / 3, as the tables above show. Pushed history is not rewritten on this
branch, so the correction lives here.
Filed, not fixed
#17453 — three
knownGaptexts indocs/qa/platform-checklist/areas/approvals.jsoncitethe now-retired
backfill-account-issuer.ts. Their own convention is the gap text staysbecause it carries the reason, so the right rewrite is a judgment call about historical
record rather than a path substitution. No gate is red on it.
🤖 Generated with Claude Code
Generated by Claude Code